//我们在学习C语言的时候讲过程序的空间分布图
//大概是这样的:

//高地址
//  |                                                 |                        
//  |                                                 |    内核空间1G                    
//  |                                                 |                        
//  |                                                ---                         
//  |                                                                        
//  |             命令行参数和环境变量                |                                           
//  |                                                 |                      
//  |                     栈区                        |                       
//  |                      |                          |                        
//  |                      |                          |                         
//  |                      |                          |                         
//  |4G                   共享区                      |                                
//  |                      |                          |    用户空间3G                     
//  |                      |                          |                         
//  |                      |                          |                         
//  |                      堆区                       |                         
//  |                                                 |                        
//  |                未初始化全局数据区               |                                
//  |                                                 |                        
//  |                初始化全局数据区                 |                                
//  |                   字符常量区                    |                        
//  |                     代码区                      |                                            
//  |                                                 |                        
//低地址                                                                           

//这个说法当时没有进行验证,现在我们来进行验证                                                                           
#include <iostream>
#include <unistd.h>
#include <stdio.h>
#include <malloc.h>

using namespace std;

//int val_one = 100;
//int val_two;
//
//int main(int argc, char* argv[], char* env[])
//{
//    //代码区
//    printf("code address: %p\n", main);
//    //字符常量区
//    const char* s = "liuqkun233!!!";
//    printf("character constant area: %p\n", s);
//    //初始化全局数据区
//    printf("init the global data area: %p\n", &val_one);
//    //未初始化全局数据区
//    printf("uninit the global data area: %p\n", &val_two);
//    //堆区
//    char* heap1 = (char*)malloc(10);
//    char* heap2 = (char*)malloc(10);
//    char* heap3 = (char*)malloc(10);
//    char* heap4 = (char*)malloc(10);
//    printf("heap1 area: %p\n", heap1);
//    printf("heap2 area: %p\n", heap2);
//    printf("heap3 area: %p\n", heap3);
//    printf("heap4 area: %p\n", heap4);
//    //栈区/堆栈区
//    printf("stack1 area: %p\n", &s);
//    printf("stack2 area: %p\n", &heap1);
//    printf("stack3 area: %p\n", &heap2);
//    printf("stack4 area: %p\n", &heap3);
//    printf("stack5 area: %p\n", &heap4);
//    //命令行参数和环境变量区
//    for (int i = 0; i < argc; ++i)
//    {
//        printf("command line area: %p\n", argv[i]);
//    }
//    for (int i = 0; env[i]; ++i)
//    {
//        printf("environment variable area: %p\n", env[i]);
//    }
//    return 0;
//}

//以前C语言说的程序地址空间我们是当作真实物理内存来讲的
//但是事实真的如此吗?我们来进行验证
//我们知道子进程继承父进程的代码(同一份代码)
//而在不修改数据的情况下,父子进程共享数据,如果子进程出现了修改数据
//那么就会发生写时拷贝,将父子进程的数据给分隔开

//下面这个代码因为没有进行数据的修改,所以没有进行写时拷贝,父子&a的值都是一样的

//int main()
//{
//    pid_t id = fork();
//
//    int a = 10;
//    if (id == 0)
//    {
//        cout << "I am child pid:" << getpid() << endl;
//        printf("the address of a is %p\n", &a);  
//        
//    }
//    else if(id > 0)
//    {
//        sleep(1);
//        cout << "I am parent pid:" << getppid() << endl;
//        printf("the address of a is %p\n", &a);  
//    }
//    else 
//    {
//        perror("fork");
//        return 1;
//    }
//    return 0;
//}

//如下代码,我们修改了数据,惊奇的发现父子进程数据的地址并没有发生变化
//如果是c/c++打印的是物理内存的地址,是根本不可能的
//说明当初我们所说的程序地址空间其本质根本就不是物理内存
//那么是什么呢?
//答:进程虚拟地址空间,是虚拟内存,打印的也是虚拟地址

int a = 10;

int main()
{
    pid_t id = fork();

    if (id == 0)
    {
        cout << "I am child pid:" << getpid() << "  a value:" << a << endl;
        printf("the address of a is %p\n", &a);  
        a = 20;
        printf("***********child change data**********\n");
        cout << "I am child pid:" << getpid() << "  a value:" << a << endl;
        printf("the address of a is %p\n", &a);  
    }
    else if(id > 0)
    {
        sleep(1);
        cout << "I am parent pid:" << getppid() << "  a value:" << a <<endl;
        printf("the address of a is %p\n", &a);  
    }
    else 
    {
        perror("fork");
        return 1;
    }
    return 0;
}

//地址空间本质是内核中的一种数据结构
//每个进程都认为自己拥有4GB
//每个进程都认为地址空间的划分是按照4GB空间进行划分的
//地址空间上进行区域划分,对应的是线性位置的虚拟地址
//虽然这里只有start和end,但是每个进程都可以认为mm_struct代表整个内存
//从最开始的地址到最后的地址范围是
//0x00000000 ~ 0xffffffff
//为什么进程地址空间里面定义的是unsigned int类型?
//可能原因可能是当时设计方面的原因
//地址本质也是个数值,是可以变成10进制的

//这些区域除了堆区和栈区其数值都是相对固定的
//栈区会根据相应的变量和函数,进行动态的调整
//堆区会根据你申请的空间进行动态的调整
//就是将end进行++多少

//struct mm_struct
//{
//    //进程地址空间
//    unsigned int code_start;
//    unsigned int code_end;
//
//    unsigned int init_data_start;
//    unsigned int init_data_end;
//
//    unsigned int uninit_data_start;
//    unsigned int uninit_data_end;
//
//    unsigned int heap_start;
//    unsigned int heap_end;
//
//    unsigned int stack_start;
//    unsigned int stack_end;
//
//    ................
//}


//那么虚拟地址和物理地址是怎么建立联系的?
//创建进程的时候,进程会有自己的task_struct、mm_struct、页表
//task_struct中会有一个指针指向mm_struct
//当我们将进程相关的数据和代码加载到内存中时,内存开辟空间进行加载
//然后将相关的映射关系写入页表
//然后内存管理单元(MMU)来进行管理页表中物理地址和虚拟地址之间的关系
//页表上就是虚拟地址和物理地址之间的映射,然后页表还可以纪录一些区域的权限
//比如这个数据是代码区和字符常量区的,通过页表映射到物理内存
//因为页表记录了映射关系的权限是只读
//那么你要进行修改时,OS系统就不会允许你进行修改
//最终我们的可执行程序的代码和数据可以加载到物理内存的任意位置
//因为最终只需要建立代码和数据与物理内存之间的映射关系
//就可以通过虚拟地址找到物理内存的对应地址

//为什么要有地址空间?
//1.通过添加一层软件层,完成有效的对内存进行风险管控(权限管理)
//本质的目的是为了,保护物理内存以及各个进程的数据安全!
//如果进程可以直接访问内存的话,那么可能因为一些非法指针,或者是写出了bug
//越界修改其他进程的数据,如果里面有账号的密码的话,不能保证其安全性
//如果有了地址空间,那么是对物理内存进行间接的访问
//那么如果是页表不存在的映射关系,那么你进行访问是不被允许的
//所以可以进一步增加对于物理内存的保护

//我们在写代码的时候肯定了解过指针越界，我们知道地址空间有各个区域
//那么指针越界一定会出现错误吗？
//不一定，越界可能他还是在自己的合法区域。比如他本来指向的是栈区
//越界后它依然指向栈区，编译器的检查机制认为这是合法的
//当你指针本来指向数据区，结果指针后来指向了字符常量区
//编译器就会根据mm_struct里面的start，end区间来判断你有没有越界
//此时发现你越界了就会报错了，这是其中的一种检查
//第二种检查为：页表因为将每个虚拟地址的区域映射到了物理内存
//其实页表也有一种权限管理，当你对数据区进行映射时
//数据区是可以读写的，相应的在页表中的映射关系中的权限就是可读可写
//但是当你对代码区和字符常量区进行映射时，因为这两个区域是只读的
//相应的在页表中的映射关系中的权限就是只读，如果你对这段区域进行了写
//通过页表当中的权限管理，操作系统就直接就将这个进程干掉。

//为什么要有地址空间?
//2.将内存申请和内存使用在时间上划分清楚,通过虚拟地址空间
//来屏蔽底层申请内存的过程,达到进程读写内存和OS进行内存管理操作
//进行软件上面的分离!
//当进程向内存申请空间的时候,只是建立了进程虚拟地址空间
//大概就是heap_end += 100,告诉进程已经申请到了空间
//但是并没有分配虚拟内存对应的物理内存
//如果你不是立马使用使用全部的空间或者是暂时不使用
//在OS的角度,如果空间立马给你,是不是意味着
//整个系统会有一部分空间可以立马给别人用的,但是现在却被你限制
//这样不利于空间的利用
//如果你这个进程现在需要读或者是写申请内存的一部分
//那么这个时候会进行基于缺页中断(现在知道有这个东西就行)的物理内存申请,
//因为OS做的内存申请动作是透明的
//所以进程并不知道其中的过程,会一直认为自己有这些申请的空间

//如果物理内存占用100%了,那么还可以加载新的进程吗?
//如果是以前的系统的话,就不可以,因为为以前没有虚拟地址空间的说法
//这样的实现过于困难
//现在linux系统有了内存管理算法,那么会将一些闲置的物理内存数据置换到磁盘上
//(那么如何进行这个物理内存空间是闲置的?
//有了空间,但是长时间没有进行读写)
//然后将这些剩余的空间赶紧拿给新进程用,用完了,把原来的数据从磁盘再置换会内存
//因为有了虚拟地址空间,页表也有相应的映射关系,那么就会变得比较容易实现

//为什么要有地址空间?
//3.站在CPU和应用层的角度,进程统一可以看做统一使用4GB空间
//而且每个空间区域的相对位置,是比较确定的!
//OS最终这样设计的目的,达到一个目标:每个进程都认为自己是独占系统资源的
//可以对每个进程进行统一的规划和管理
//假如如果没有虚拟地址空间,那么执行进程的时候,CPU代码的入口是很麻烦的
//CPU将磁盘中的数据和代码加载到物理内存中
//那么不同的进程,就会有不同的代码的入口,增加了CPU管理内存的负担
//有了虚拟地址空间,那么因为代码区的范围是比较固定的
//那么,意味着,每个进程的入口都可以被控制在这个范围
//CPU查找就会很方便
//这样程序的代码和数据可以被加载到物理内存的任意位置
//通过页表建立映射关系,大大减少了内存管理的负担

//通过上面的更多的认识，那么现在我们对于进程的认识更加进一步
//进程 = 相关的代码 + PCB + 页表 + mm_struct + 内核相关数据结构
